package main

import (
    "bytes"
    "crypto/des"
    "encoding/base64"
    "encoding/json"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "os/exec"
    "strings"
    "time"
)

const (
    desKey = "ourheyday-yuandaima"
)

type DeviceConfig struct {
    WxMfrsId string `json:"wxMfrsId"`
    WxTypeId string `json:"wxTypeId"`
    WxType   int    `json:"wxType"`
}

// 配置默认值全部为零
var deviceCfg = DeviceConfig{}

func printHelp() {
    fmt.Println(`Usage:
  push_wxy --api-url <push api url> --config-url <remote config url> [--interval <minutes>]
Options:
  --api-url     Set push API url (e.g. https://host/api/mgtCmdDvc/push)
  --interval    Push interval in minutes (default 5)
  --config-url  Set url to pull wxMfrsId/wxTypeId/wxType json config (MUST be set)
  --help        Print this help
Description:
  This tool gathers device info from WangxinYun local unix socket and sends device push info for ONLY WangxinYun model,
  with all encryption done by built-in DES/ECB/PKCS5 & BASE64 as required.
  wxMfrsId/typeId/type must be configured from --config-url.`)
}

func pkcs5Padding(src []byte, blockSize int) []byte {
    pad := blockSize - len(src)%blockSize
    padding := bytes.Repeat([]byte{byte(pad)}, pad)
    return append(src, padding...)
}

func pkcs5Unpadding(src []byte) []byte {
    l := len(src)
    if l == 0 {
        return src
    }
    pad := int(src[l-1])
    if pad > l {
        return src
    }
    return src[:l-pad]
}

func desEncrypt(plain string) (string, error) {
    key := []byte(desKey)[:8]
    block, err := des.NewCipher(key)
    if err != nil {
        return "", err
    }
    data := pkcs5Padding([]byte(plain), block.BlockSize())
    encrypted := make([]byte, len(data))
    for bs, be := 0, block.BlockSize(); bs < len(data); bs, be = bs+block.BlockSize(), be+block.BlockSize() {
        block.Encrypt(encrypted[bs:be], data[bs:be])
    }
    return base64.StdEncoding.EncodeToString(encrypted), nil
}

func desDecrypt(cipherTextBase64 string) (string, error) {
    key := []byte(desKey)[:8]
    encrypted, err := base64.StdEncoding.DecodeString(cipherTextBase64)
    if err != nil {
        return "", err
    }
    block, err := des.NewCipher(key)
    if err != nil {
        return "", err
    }
    if len(encrypted)%block.BlockSize() != 0 {
        return "", fmt.Errorf("input not multiple of blocksize")
    }
    decrypted := make([]byte, len(encrypted))
    for bs, be := 0, block.BlockSize(); bs < len(encrypted); bs, be = bs+block.BlockSize(), be+block.BlockSize() {
        block.Decrypt(decrypted[bs:be], encrypted[bs:be])
    }
    decrypted = pkcs5Unpadding(decrypted)
    return string(decrypted), nil
}

func getWangxinInfo() (dvcCode, dvcId, ohdCode string, err error) {
    cmd := exec.Command("curl",
        "-s",
        "-X", "POST",
        "--unix-socket", "/data/.lotso/.wxsock",
        "http://localhost/v1.0/plugin/device_info")
    out, err := cmd.Output()
    if err != nil {
        return "", "", "", fmt.Errorf("unable to request WangxinYun local unix-socket: %v", err)
    }
    var resp struct {
        IRet int    `json:"iRet"`
        SMsg string `json:"sMsg"`
        Data struct {
            ActivateCode string `json:"activate_code"`
            Sn           string `json:"sn"`
        } `json:"data"`
    }
    if err := json.Unmarshal(out, &resp); err != nil {
        return "", "", "", fmt.Errorf("unable to parse WangxinYun info: %v\nRaw response: %s", err, out)
    }
    if resp.IRet != 0 {
        return "", "", "", fmt.Errorf("WangxinYun device_info api replied nonzero iRet: %d, msg: %s", resp.IRet, resp.SMsg)
    }
    dvcCode = resp.Data.ActivateCode
    dvcId = resp.Data.Sn

    sn := ""
    content, err2 := ioutil.ReadFile("/proc/cmdline")
    if err2 == nil {
        fields := strings.Fields(string(content))
        for _, field := range fields {
            if strings.HasPrefix(field, "snum=") {
                sn = strings.TrimPrefix(field, "snum=")
                break
            }
        }
    }
    if sn == "" {
        machineId, err3 := ioutil.ReadFile("/etc/machine-id")
        if err3 == nil {
            sn = strings.TrimSpace(string(machineId))
        }
    }
    if dvcCode == "" || dvcId == "" {
        return "", "", "", fmt.Errorf("activate_code or sn not found in WangxinYun device_info")
    }
    if sn == "" {
        return "", "", "", fmt.Errorf("unable to get real SN from /proc/cmdline or /etc/machine-id")
    }
    ohdCode = sn
    return dvcCode, dvcId, ohdCode, nil
}

func logInfo(format string, args ...interface{}) {
    fmt.Printf("[%s] ", time.Now().Format("2006-01-02 15:04:05"))
    fmt.Printf(format, args...)
}

func checkBindIdFlag() bool {
    flagPath := "/etc/bindid.flag"
    content, err := ioutil.ReadFile(flagPath)
    if err != nil {
        logInfo("Flag file %s not found or cannot be read: %v\n", flagPath, err)
        return false
    }
    
    stripped := strings.TrimSpace(string(content))
    if len(stripped) < 5 {
        logInfo("Flag file %s content too short (need >=5 chars, got %d): '%s'\n", flagPath, len(stripped), stripped)
        return false
    }
    
    logInfo("Flag file %s valid, content length: %d\n", flagPath, len(stripped))
    return true
}

// 只能远程获得配置
func loadDeviceConfig(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return fmt.Errorf("failed to fetch remote config: %v", err)
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("failed to read config response: %v", err)
    }
    var tmp DeviceConfig
    if err := json.Unmarshal(body, &tmp); err != nil {
        return fmt.Errorf("failed to parse config json: %v", err)
    }
    // 字段校验，必须都非空/非0
    if tmp.WxMfrsId == "" || tmp.WxTypeId == "" || tmp.WxType == 0 {
        return fmt.Errorf("config json incomplete: all of wxMfrsId/wxTypeId/wxType must be set, got: %+v", tmp)
    }
    deviceCfg = tmp
    return nil
}

func main() {
    apiUrl := flag.String("api-url", "", "Set push API url")
    configUrl := flag.String("config-url", "", "Remote config url for wxMfrsId/wxTypeId/wxType (REQUIRED)")
    intervalMin := flag.Int("interval", 5, "Push interval in minutes (default 5)")
    showHelp := flag.Bool("help", false, "Print help")
    flag.Usage = printHelp
    flag.Parse()
    if *showHelp {
        printHelp()
        return
    }
    if *apiUrl == "" {
        fmt.Println("Error: --api-url is required.")
        printHelp()
        os.Exit(1)
    }
    if *configUrl == "" {
        fmt.Println("Error: --config-url is required and must point to a valid JSON config file.")
        printHelp()
        os.Exit(1)
    }
    if *intervalMin <= 0 {
        fmt.Println("Error: --interval must be positive.")
        os.Exit(1)
    }

    logInfo("Loading config from %s\n", *configUrl)
    if err := loadDeviceConfig(*configUrl); err != nil {
        logInfo("[ERROR] %v\n", err)
        os.Exit(2)
    }
    logInfo("Loaded remote config: %+v\n", deviceCfg)

    logInfo("Starting WangxinYun periodic push, interval = %dmin\n", *intervalMin)

    ticker := time.NewTicker(time.Duration(*intervalMin) * time.Minute)
    defer ticker.Stop()

    for {
        func() {
            if !checkBindIdFlag() {
                logInfo("Skipping this cycle due to invalid flag file\n")
                return
            }
            
            logInfo("Collecting device info...\n")
            dvcCode, dvcId, ohdCode, err := getWangxinInfo()
            if err != nil {
                logInfo("[ERROR] %v\n", err)
                return
            }

            inner := map[string]interface{}{
                "dvcCode": dvcCode,
                "dvcId":   dvcId,
                "ohdCode": ohdCode,
                "mfrsId":  deviceCfg.WxMfrsId,
                "typeId":  deviceCfg.WxTypeId,
                "type":    deviceCfg.WxType,
            }
            bodyBytes, _ := json.Marshal(inner)
            logInfo("Payload before encryption (plaintext JSON):\n%s\n", string(bodyBytes))

            encBody, err := desEncrypt(string(bodyBytes))
            if err != nil {
                logInfo("[ERROR] Encryption failed: %v\n", err)
                return
            }
            logInfo("Payload after DES/ECB/PKCS5->Base64:\n%s\n", encBody)

            reqPack := map[string]interface{}{
                "result": true,
                "data":   encBody,
            }
            reqBytes, _ := json.Marshal(reqPack)
            logInfo("Request JSON to API:\n%s\n", string(reqBytes))

            req, err := http.NewRequest("POST", *apiUrl, bytes.NewReader(reqBytes))
            if err != nil {
                logInfo("[ERROR] Create HTTP request failed: %v\n", err)
                return
            }
            req.Header.Set("Content-Type", "application/json")
            client := &http.Client{}

            resp, err := client.Do(req)
            if err != nil {
                logInfo("[ERROR] POST to API failed: %v\n", err)
                return
            }
            respBytes, _ := ioutil.ReadAll(resp.Body)
            resp.Body.Close()

            logInfo("API raw response:\n%s\n", string(respBytes))

            // 解密返回内容（按约定提取data字段）
            var respJson map[string]interface{}
            var respEncrypted, respDecrypted string
            if err := json.Unmarshal(respBytes, &respJson); err == nil {
                if d, ok := respJson["data"].(string); ok && d != "" {
                    respEncrypted = d
                    if dec, err := desDecrypt(respEncrypted); err == nil {
                        respDecrypted = dec
                    } else {
                        respDecrypted = fmt.Sprintf("[Decryption error: %v]", err)
                    }
                }
            }
            logInfo("Response 'data' encrypted:\n%s\n", respEncrypted)
            logInfo("Response 'data' decrypted:\n%s\n", respDecrypted)
        }()
        logInfo("Wait %d minutes until next report...\n\n", *intervalMin)
        <-ticker.C
    }
}



